/** 
 * @description 询问树提示插件
 * @author 原作者: @see https://github.com/insightfuls/inquirer-tree-prompt
 * -- 此代码在原作者基础上修改了选中的颜色以及提示信息，以及修改选择为空的报错处理。
*/

const _ = {
  cloneDeep: require('lodash/cloneDeep')
};
const chalk = require('chalk');
const figures = require('figures');
const cliCursor = require('cli-cursor');
const { fromEvent } = require('rxjs');
const { filter, share, flatMap, map, take, takeUntil } = require('rxjs/operators');
const BasePrompt = require('inquirer/lib/prompts/base');
const observe = require('inquirer/lib/utils/events');
const Paginator = require('inquirer/lib/utils/paginator');

class TreePrompt extends BasePrompt {

  constructor(questions, rl, answers) {
    super(questions, rl, answers);

    // eslint-disable-next-line no-empty-function
    this.done = () => {};

    this.firstRender = true;

    const tree = typeof this.opt.tree === 'function'
      ? this.opt.tree : _.cloneDeep(this.opt.tree);

    this.tree = { children: tree };

    this.shownList = [];

    this.opt = {
      pageSize: 10,
      multiple: false,
      ...this.opt
    }

    // Make sure no default is set (so it won't be printed)
    this.opt.default = null;

    this.paginator = new Paginator(this.screen, { isInfinite: this.opt.loop !== false });

    this.selectedList = [];
  }

  /**
	 * @protected
	 */
  async _run(done) {
    this.done = done;

    this._installKeyHandlers();

    cliCursor.hide();

    await this.prepareChildrenAndRender(this.tree);

    // TODO: exit early somehow if no items
    // TODO: what about if there are no valid items?

    return this;
  }

  _installKeyHandlers() {
    const events = observe(this.rl);

    const validation = this.handleSubmitEvents(
      events.line.pipe(map(() => this.valueFor(
        this.opt.multiple ? this.selectedList[0] : this.active))));
    validation.success.forEach(this.onSubmit.bind(this));
    validation.error.forEach(this.onError.bind(this));

    events.normalizedUpKey
      .pipe(takeUntil(validation.success))
      .forEach(this.onUpKey.bind(this));

    events.normalizedDownKey
      .pipe(takeUntil(validation.success))
      .forEach(this.onDownKey.bind(this));

    events.keypress.pipe(
      filter(({ key }) => key.name === 'right'),
      share()
    )
      .pipe(takeUntil(validation.success))
      .forEach(this.onRightKey.bind(this));

    events.keypress.pipe(
      filter(({ key }) => key.name === 'left'),
      share()
    )
      .pipe(takeUntil(validation.success))
      .forEach(this.onLeftKey.bind(this));

    events.spaceKey
      .pipe(takeUntil(validation.success))
      .forEach(this.onSpaceKey.bind(this));

    function normalizeKeypressEvents(value, key) {
      return { value, key: key || {} };
    }
    fromEvent(this.rl.input, 'keypress', normalizeKeypressEvents)
      .pipe(filter(({ key }) => key && key.name === 'tab'), share())
      .pipe(takeUntil(validation.success))
      .forEach(this.onTabKey.bind(this));
  }

  async prepareChildrenAndRender(node) {
    await this.prepareChildren(node);

    this.render();
  }

  async prepareChildren(node) {
    if (node.prepared) {
      return;
    }
    node.prepared = true;

    await this.runChildrenFunctionIfRequired(node);

    if (!node.children) {
      return;
    }

    this.cloneAndNormaliseChildren(node);

    await this.validateAndFilterDescendants(node);
  }

  async runChildrenFunctionIfRequired(node) {
    if (typeof node.children === 'function') {
      try {
        const nodeOrChildren = await node.children();
        if (nodeOrChildren) {
          let children;
          if (Array.isArray(nodeOrChildren)) {
            children = nodeOrChildren;
          } else {
            children = nodeOrChildren.children;
            [ 'name', 'value', 'short' ].forEach((property) => {
              node[property] = nodeOrChildren[property];
            });
            node.isValid = undefined;

            await this.addValidity(node);

            /*
						 * Don't filter based on validity; children can be handled by the
						 * callback itself if desired, and filtering out the node itself
						 * would be a poor experience in this scenario.
						 */
          }

          node.children = _.cloneDeep(children);
        }
      } catch (e) {
        /*
				 * if something goes wrong gathering the children, ignore it;
				 * it could be something like permission denied for a single
				 * directory in a file hierarchy
				 */

        node.children = null;
      }
    }
  }

  cloneAndNormaliseChildren(node) {
    node.children = node.children.map((item) => {
      if (typeof item !== 'object') {
        return {
          value: item
        };
      }

      return item;
    });
  }

  async validateAndFilterDescendants(node) {
    for (let index = node.children.length - 1; index >= 0; index--) {
      const child = node.children[index];

      child.parent = node;

      await this.addValidity(child);

      if (this.opt.hideChildrenOfValid && child.isValid === true) {
        child.children = null;
      }

      if (this.opt.onlyShowValid && child.isValid !== true && !child.children) {
        node.children.splice(index, 1);
      }

      if (child.open) {
        await this.prepareChildren(child);
      }
    }
  }

  async addValidity(node) {
    if (typeof node.isValid === 'undefined') {
      if (this.opt.validate) {
        node.isValid = await this.opt.validate(this.valueFor(node), this.answers);
      } else {
        node.isValid = true;
      }
    }
  }

  render(error) {
    let message = this.getQuestion();

    if (this.firstRender) {
      let hint = '使用箭头按键移动，上下键移动，右键展开子模块，左键收起子模块，';
      if (this.opt.multiple) {
        hint += '空格键选择，';
      }
      hint += '回车键确认。';

      message += chalk.green(`(${hint})`);
    }

    if (this.status === 'answered') {
      let answer;
      if (this.opt.multiple) {
        answer = this.selectedList.map((item) => this.shortFor(item, true)).join(', ');
      } else {
        answer = this.shortFor(this.active, true);
      }

      message += chalk.cyan(answer);
    } else {
      this.shownList = [];
      
      let treeContent = '\n'
      
      treeContent += 'start 分割线 ---------- \n'
      treeContent += this.createTreeContent();
      
      if (this.opt.loop !== false) {

        treeContent += 'end   分割线 ----------';
        treeContent += '\n';
      }
      
      message += `\n${ this.paginator.paginate(treeContent,
        this.shownList.indexOf(this.active), this.opt.pageSize)}`;
    }

    let bottomContent;

    if (error) {
      bottomContent = `\n${ chalk.red('>> ') }${error}`;
    }

    this.firstRender = false;

    this.screen.render(message, bottomContent);
  }

  createTreeContent(node = this.tree, indent = 2) {
    const children = node.children || [];
    let output = '';
    const isFinal = this.status === 'answered';

    children.forEach(child => {
      this.shownList.push(child)
      
      if (!this.active) {
        this.active = child;
      }

      let prefix = child.children
        ? child.open
          ? `${figures.arrowDown } `
          : `${figures.arrowRight } `
        : child === this.active
          ? `${figures.pointer } `
          : '  '

      if (this.opt.multiple) {
        prefix += this.selectedList.includes(child) ? chalk.green(figures.radioOn) : figures.radioOff;
        prefix += ' ';
      }

      const showValue = `${' '.repeat(indent) + prefix + this.nameFor(child, isFinal) }\n`;

      if (child === this.active) {
        if (child.isValid === true) {
          output += chalk.cyan(showValue)
        } else {
          output += chalk.red(showValue)
        }
      }
      else {
        output += showValue
      }

      if (child.open) {
        output += this.createTreeContent(child, indent + 2)
      }
    })

    return output
  }

  shortFor(node, isFinal = false) {
    return typeof node.short !== 'undefined' ? node.short : this.nameFor(node, isFinal);
  }

  nameFor(node, isFinal = false) {
    if (typeof node.name !== 'undefined') {
      return node.name;
    }

    if (this.opt.transformer) {
      return this.opt.transformer(node.value, this.answers, { isFinal });
    }

    return node.value;
  }

  valueFor(node) {
    if (!node) {
      return ''
    }
    
    return typeof node.value !== 'undefined' ? node.value : node.name;
  }

  onError(state) {
    this.render(state.isValid);
  }

  onSubmit(state) {
    this.status = 'answered';

    this.render();

    this.screen.done();
    cliCursor.show();

    this.done(this.opt.multiple
      ? this.selectedList.map((item) => this.valueFor(item)) : state.value);
  }

  onUpKey() {
    this.moveActive(-1);
  }

  onDownKey() {
    this.moveActive(1);
  }

  onLeftKey() {
    if (this.active.children && this.active.open) {
      this.active.open = false;
    } else {
      if (this.active.parent !== this.tree) {
        this.active = this.active.parent;
      }
    }

    this.render();
  }

  onRightKey() {
    if (this.active.children) {
      if (!this.active.open) {
        this.active.open = true

        this.prepareChildrenAndRender(this.active);
      } else if (this.active.children.length) {
        this.moveActive(1);
      }
    }
  }

  moveActive(distance = 0) {
    const currentIndex = this.shownList.indexOf(this.active);
    let index = currentIndex + distance;

    if (index >= this.shownList.length) {
      if (this.opt.loop === false) {
        return;
      }
      index = 0;
    }
    else if (index < 0) {
      if (this.opt.loop === false) {
        return;
      }
      index = this.shownList.length - 1;
    }

    this.active = this.shownList[index];

    this.render();
  }

  onTabKey() {
    this.toggleOpen();
  }

  onSpaceKey() {
    if (this.opt.multiple) {
      this.toggleSelection();
    } else {
      this.toggleOpen();
    }
  }

  toggleSelection() {
    if (this.active.isValid !== true) {
      return;
    }

    const selectedIndex = this.selectedList.indexOf(this.active);
    if (selectedIndex === -1) {
      this.selectedList.push(this.active);
    } else {
      this.selectedList.splice(selectedIndex, 1);
    }

    this.render();
  }

  toggleOpen() {
    if (!this.active.children) {
      return;
    }

    this.active.open = !this.active.open;

    this.render();
  }

}

module.exports = TreePrompt;
